package hw2.cs236369.extensions;

import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;

/**
 * Proves LINQ like extension methods
 * 
 * @author Gal Lalouche
 * 
 */
public class Extender {
	/**
	 * count all the elements in the iterable
	 * 
	 * @param iterable the iterable to count
	 * @return The number of elements in the iterable
	 */
	public static <T> int count(Iterable<T> iterable) {
		// if collection, return its size
		if (iterable instanceof Collection<?>) {
			return ((Collection<?>)iterable).size();
		}

		// return count with true
		return count(iterable, new Tautology<T>());
	}

	/**
	 * count all the elements in the iterable that satisfy a given condition
	 * 
	 * @param iterable the iterable to count
	 * @param f The predicate, should return true iff the element should be
	 *            counted
	 * @return The number of elements in the iterable that satisfy the predicate
	 */
	public static <T> int count(Iterable<? extends T> iterable, Predicate<T> f) {
		int $ = 0;
		// only count items that satisfy the condition
		for (T element : iterable) {
			if (f.calculate(element)) {
				$++;
			}
		}
		return $;
	}

	/**
	 * select only the elements which satisfy a given condition
	 * 
	 * @param iterable The iterable to filter
	 * @param f The predicate, should true iff the element should be selected
	 * @return A collection of only elemenets that satisfy the condition
	 */
	public static <T> Collection<T> where(Iterable<T> iterable, Predicate<T> f) {
		Collection<T> $ = new LinkedList<T>();
		for (T element : iterable) {
			if (f.calculate(element)) {
				$.add(element);
			}
		}
		return $;
	}

	/**
	 * Returns the first element in iterable
	 * 
	 * @param iterable The iterable to return the first element of
	 * @throws IllegalStateException If the iterable is empty
	 * @return The first element in the iterable
	 */
	public static <T> T first(Iterable<T> iterable) throws IllegalStateException {
		return first(iterable, new Tautology<T>());
	}

	/**
	 * Returns the first element in iterable that satisfies a given condition
	 * 
	 * @param iterable The iterable to return the first element of
	 * @param f The predicate, should be true iff the element satisfies the
	 *            condition
	 * @throws IllegalStateException If no element satisfies the condition
	 * @return The first element in the iterable that satisfies a given
	 *         condition
	 */
	public static <T> T first(Iterable<T> iterable, Predicate<T> f) throws IllegalStateException {
		for (T element : iterable) {
			if (f.calculate(element)) {
				return element;
			}
		}

		throw new IllegalStateException("iterable is empty or no element satisfies the predicate");
	}

	/**
	 * Returns the last element in iterable
	 * 
	 * @param iterable The iterable to return the last element of
	 * @throws IllegalStateException If the iterable is empty
	 * @return The last element in the iterable
	 */
	public static <T> T last(Iterable<T> iterable) {
		return last(iterable, new Tautology<T>());
	}

	/**
	 * Returns the last element in iterable that satisfies a given condition
	 * 
	 * @param iterable The iterable to return the last element of
	 * @param predicate The predicate, should be true iff the element satisfies the
	 *            condition
	 * @throws IllegalStateException If no element satisfies the condition
	 * @return The last element in the iterable that satisfies a given condition
	 */
	public static <T> T last(Iterable<T> iterable, Predicate<T> predicate) {
		T last = null;
		boolean wasFound = false;

		for (T element : iterable) {
			if (predicate.calculate(element)) {
				wasFound = true;
				last = element;
			}
		}

		if (wasFound) {
			return last;
		} else {
			throw new IllegalStateException("iterable is empty or no element satisfies the predicate");
		}
	}

	/**
	 * Turns the iterable into a collection
	 * 
	 * @param iterable The iterable to turn in to a collection
	 * @return A collection with the same elements as the iterable
	 */
	public static <T> Collection<T> toCollection(Iterable<T> iterable) {
		// if already a collection, return it
		if (iterable instanceof Collection<?>) {
			return (Collection<T>)iterable;
		}
		// where with all elements
		return where(iterable, new Tautology<T>());
	}

	/**
	 * Turns the array into a collection
	 * 
	 * @param arr The array to turn in to a collection
	 * @return A collection with the same elements as the array
	 */
	public static <T> Collection<T> toCollection(T[] arr) {
		Collection<T> $ = new LinkedList<T>();
		for (int i = 0; i < arr.length; i++) {
			$.add(arr[i]);
		}

		return $;
	}

	/**
	 * Check if any element in the iterable satisfies the given condition
	 * 
	 * @param iterable The iterable to check
	 * @param f The predicate, should true iff the element satisfies the
	 *            condition
	 * @return True iff there is an item that satisfies the condition
	 */
	public static <T> boolean any(Iterable<T> iterable, Predicate<T> f) {
		for (T element : iterable) {
			if (f.calculate(element)) {
				return true;
			}
		}

		return false;
	}

	/**
	 * Check if all element in the iterable satisfy the given condition
	 * 
	 * @param iterable The iterable to check
	 * @param f The predicate, should true iff the element satisfies the
	 *            condition
	 * @return True iff there all items satisfy the condition
	 */
	public static <T> boolean all(Iterable<T> iterable, final Predicate<T> f) {
		// check that the no element satisfies the negative condition
		return where(iterable, new Predicate<T>() {
			@Override
			public Boolean calculate(T element) {
				// not(f)
				return f.calculate(element) == false;
			}
		}).isEmpty();
	}

	/**
	 * Check if the iterable is empty
	 * 
	 * @param iterable The iterable to check
	 * @return True iff the iterable is empty
	 */
	public static <T> boolean isEmpty(Iterable<T> iterable) {
		for (@SuppressWarnings("unused")
		T element : iterable) {
			return true;
		}
		return false;
	}

	/**
	 * Constructs a dictionary from the iterable
	 * 
	 * @param <V> The iterable type
	 * @param <K> The key type
	 * @param iterable The iterable to create the dictionary from
	 * @param f A mapping function from an element to a key
	 * @return A dictionary from the iterable, where every value is an element
	 *         in the iterable And every key is f(value);
	 */
	public static <K, V> Map<K, V> toDictionary(Iterable<V> iterable, Function<V, K> f) {
		Map<K, V> $ = new HashMap<K, V>();
		for (V element : iterable) {
			$.put(f.calculate(element), element);
		}

		return $;
	}

	/**
	 * Map the iterable to the a new collection
	 * 
	 * @param <T> The iterable type
	 * @param <S> The new collection type
	 * @param iterable The iterable to map
	 * @param f The mapping function (T -> S)
	 * @return The mapped collection
	 */
	public static <T, S> Collection<S> map(Iterable<T> iterable, Function<T, S> f) {
		LinkedList<S> $ = new LinkedList<S>();
		for (T element : iterable) {
			$.add(f.calculate(element));
		}

		return $;
	}

	/**
	 * Returns an iterable of numbers in the [min, max) range, in jumps of
	 * interval
	 * 
	 * @param max The last item in the iterable (exlusive)
	 * @return An iterable of numbers in the [0, max) range
	 */
	public static Iterable<Integer> range(int max) {
		return range(0, max);
	}

	/**
	 * Returns an iterable of numbers in the [min, max) range, in jumps of
	 * interval
	 * 
	 * @param start The first item in the iterable (inclusive)
	 * @param end The last item in the iterable (exclusive)
	 * @return An iterable of numbers in the [min, max) range
	 */
	public static Iterable<Integer> range(int start, int end) {
		return range(start, end, 1);
	}

	/**
	 * Returns an iterable of numbers in the [min, max) range, in jumps of
	 * interval
	 * 
	 * @param start The first item in the iterable (inclusive)
	 * @param end The last item in the iterable (exclusive)
	 * @param interval The interval between each element
	 * @return An iterable of numbers in the [min, max) range, in jumps of
	 *         interval
	 */
	public static Iterable<Integer> range(int start, int end, int interval) {
		Collection<Integer> $ = new LinkedList<Integer>();
		for (int i = start; i < end; i += interval) {
			$.add(i);
		}

		return $;
	}

	/**
	 * Returns the maximal element in the iterable
	 * 
	 * @param iterable
	 * @return The largest item in the iterable
	 */
	public static Number max(Iterable<? extends Number> iterable) {
		double max = Double.MIN_VALUE;

		for (Number num : iterable) {
			max = Math.max(max, num.doubleValue());
		}
		return max;
	}

	/**
	 * Returns the minmal element in the iterable
	 * 
	 * @param iterable
	 * @return The smallest item in the iterable
	 */
	public static Number min(Iterable<? extends Number> iterable) {
		double min = Double.MAX_VALUE;

		for (Number num : iterable) {
			min = Math.min(min, num.doubleValue());
		}
		return min;
	}

	/**
	 * Takes the first n elements of an iterable
	 * 
	 * @param iterable The iterable to take the first n elements of
	 * @param n The number of element to take; if this size is bigger than the
	 *            size of the iterable, all elements will be taken
	 * @return The first n elements of the iterable
	 */
	public static <T> Iterable<T> take(Iterable<T> iterable, int n) {
		Collection<T> $ = new LinkedList<T>();

		if (iterable instanceof Collection<?> && count(iterable) <= n) {
			return iterable;
		}

		int i = 0;
		for (T element : iterable) {
			if (i < n) {
				break;
			}
			$.add(element);
		}

		return $;
	}

	/**
	 * Skips the first n elements of an iterable
	 * 
	 * @param iterable The iterable to take the first n elements of
	 * @param n The number of element to take; if this size is bigger than the
	 *            size of the iterable, no elements will be taken
	 * @return The given iterable, with the first n elements skipped
	 */
	public static <T> Iterable<T> skip(Iterable<T> iterable, int n) {
		int i = 0;
		Collection<T> $ = new LinkedList<>();
		for (T element : iterable) {
			if (i++ >= n) {
				$.add(element);
			}
		}

		return $;
	}

	/**
	 * Sums all the elements in the iterable
	 * 
	 * @param iterable
	 * @return The sum of all the elements in the iterable
	 */
	public static Number sum(Iterable<? extends Number> iterable) {
		return aggregate(iterable, new Aggregator<Number, Number>() {
			@Override
			public Number calculate(Number currentValue, Number newValue) {
				return currentValue.doubleValue() + newValue.doubleValue();
			}
		}, 0.);
	}

	/**
	 * Calculates the product of all the elements in the iterable
	 * 
	 * @param iterable
	 * @return The product of all the elements in the iterable
	 */
	public static Number product(Iterable<? extends Number> iterable) {
		return aggregate(iterable, new Aggregator<Number, Number>() {
			@Override
			public Number calculate(Number currentValue, Number newValue) {
				return currentValue.doubleValue() * newValue.doubleValue();
			}
		}, 1.);
	}

	/**
	 * Aggregates over the iterable
	 * 
	 * @param iterable The iterable to aggregate over
	 * @param aggregator The aggregation function
	 * @param startingValue The starting value, all elements will be aggregated
	 *            to this value
	 * @return The aggregated value
	 */
	public static <T, S> S aggregate(Iterable<? extends T> iterable, Aggregator<S, T> aggregator, S startingValue) {
		S aggratedValue = startingValue;
		for (T element : iterable) {
			aggratedValue = aggregator.calculate(aggratedValue, element);
		}

		return aggratedValue;
	}

	/**
	 * Reverses the array in place
	 * 
	 * @param arr The array to reverse
	 */
	public static <T> void reverse(T[] arr) {
		for (int i = 0; i < arr.length / 2; i++) {
			T temp = arr[i];
			int mirrorI = arr.length - 1 - i;
			arr[i] = arr[mirrorI];
			arr[mirrorI] = temp;
		}
	}

	/**
	 * Reverses the iterable
	 * 
	 * @param iterable The iterable to reverse
	 * @return An iterable with the elements in reverse order
	 */
	public static <T> Iterable<T> reverse(Iterable<T> iterable) {
		LinkedList<T> $ = new LinkedList<T>();
		for (T element : iterable) {
			$.addFirst(element);
		}

		return $;
	}

	/**
	 * Transforms the array to a collection of integers
	 * 
	 * @param array The array to turn to a collecion
	 * @return A collection with the array's elements
	 */
	public static Collection<Integer> toCollection(int[] array) {
		Collection<Integer> $ = new LinkedList<Integer>();
		for (int i = 0; i < array.length; i++) {
			$.add(array[i]);
		}

		return $;
	}

	/**
	 * Generates a print friendly version of the iterable
	 * 
	 * @param iterable The iterable
	 * @return A print friendly version of the iterable "[e1, e2, ..., en]"
	 */
	public static <T> String toString(Iterable<T> iterable) {
		StringBuilder sb = new StringBuilder();
		int i = 0;
		int size = count(iterable);
		sb.append("[");
		for (T element : iterable) {
			sb.append(element);
			if (++i >= size) {
				break;
			}
			sb.append(", ");
		}

		sb.append("]");

		return sb.toString();
	}

	/**
	 * Generates a print friendly version of the array
	 * 
	 * @param arr The array
	 * @return A print friendly version of the array "[e1, e2, ..., en]"
	 */
	public static <T> String toString(T[] arr) {
		StringBuilder sb = new StringBuilder();
		int i = 0;
		int size = arr.length;
		sb.append("[");
		for (T element : arr) {
			sb.append(element);
			if (++i >= size) {
				break;
			}
			sb.append(", ");
		}

		sb.append("]");

		return sb.toString();
	}

	/**
	 * Trasforms the collection to a list
	 * 
	 * @param iterable The iterable to turn
	 * @return A list made of up of the elemnts of the iterable
	 */
	public static <T> List<T> toList(Iterable<T> iterable) {
		if (iterable instanceof List<?>) {
			return (List<T>)iterable;
		}

		return new LinkedList<T>(toCollection(iterable));
	}

	/**
	 * Groups the elements by a key
	 * 
	 * @param iterable The iterable to group
	 * @param f A function from element -> key
	 * @return A collection of collections of elements grouped by key
	 */
	public static <T, S> Collection<? extends Collection<T>> groupBy(Iterable<T> iterable, Function<T, S> f) {
		Map<S, Collection<T>> $ = new HashMap<S, Collection<T>>();

		for (T element : iterable) {
			S result = f.calculate(element);
			if ($.containsKey(result) == false) {
				$.put(result, new LinkedList<T>());
			}

			$.get(result).add(element);
		}

		return $.values();
	}

	/**
	 * Returns the average of all the elements in the iterable
	 * 
	 * @param iterable The iterable to return the average of
	 * @return The average of all the elements in the iterable
	 */
	public static double average(Iterable<? extends Number> iterable) {
		return sum(iterable).doubleValue() / count(iterable);
	}

	/**
	 * Checks if a given element is contained in the iterable
	 * @param iterable The iterable to check
	 * @param element The element to check
	 * @return True iff the element contains the iterable
	 */
	public static <T> boolean contains(Iterable<T> iterable, final T element) {
		if (iterable instanceof Collection) {
			return ((Collection<T>)iterable).contains(element);
		}
		
		return any(iterable, new Predicate<T>() {
			@Override
			public Boolean calculate(T e) {
				return element.equals(e);
			}
		});
	}

	/**
	 * Orders the element by a given function
	 * @param iterable The iterable to order
	 * @param f The function from T -> element to order by
	 * @return A new ordered element
	 */
	public static <T, S extends Comparable<S>> Iterable<T> orderBy(Iterable<T> iterable, final Function<T, S> f) {
		List<T> collection = toList(iterable);
		Collections.sort(collection, new Comparator<T>() {
			@Override
			public int compare(T arg0, T arg1) {
				S s0 = f.calculate(arg0);
				S s1 = f.calculate(arg1);
				return s0.compareTo(s1);
			}

		});

		return collection;
	}
}
